/**
 * @fileOverview Undo/Redo system for saving shapshot for document modification
 *      and other recordable changes.
 */
(function() {
   VED.plugins.add( 'undo', {
      lang: 'en,ru', // %REMOVE_LINE_CORE%
      icons: 'redo,redo-rtl,undo,undo-rtl', // %REMOVE_LINE_CORE%
      init: function( editor ) {
         var undoManager = new UndoManager( editor );

         var undoCommand = editor.addCommand( 'undo', {
            exec: function() {
               if ( undoManager.undo() ) {
                  editor.selectionChange();
                  this.fire( 'afterUndo' );
               }
            },
            state: VED.TRISTATE_DISABLED,
            canUndo: false
         });

         var redoCommand = editor.addCommand( 'redo', {
            exec: function() {
               if ( undoManager.redo() ) {
                  editor.selectionChange();
                  this.fire( 'afterRedo' );
               }
            },
            state: VED.TRISTATE_DISABLED,
            canUndo: false
         });

         editor.setKeystroke( [
            [ VED.CTRL + 90 /*Z*/, 'undo' ],
            [ VED.CTRL + 89 /*Y*/, 'redo' ],
            [ VED.CTRL + VED.SHIFT + 90 /*Z*/, 'redo' ]
            ] );

         undoManager.onChange = function() {
            undoCommand.setState( undoManager.undoable() ? VED.TRISTATE_OFF : VED.TRISTATE_DISABLED );
            redoCommand.setState( undoManager.redoable() ? VED.TRISTATE_OFF : VED.TRISTATE_DISABLED );
         };

         function recordCommand( event ) {
            // If the command hasn't been marked to not support undo.
            if ( undoManager.enabled && event.data.command.canUndo !== false )
               undoManager.save();
         }

         // We'll save snapshots before and after executing a command.
         editor.on( 'beforeCommandExec', recordCommand );
         editor.on( 'afterCommandExec', recordCommand );

         // Save snapshots before doing custom changes.
         editor.on( 'saveSnapshot', function( evt ) {
            undoManager.save( evt.data && evt.data.contentOnly );
         });

         // Registering keydown on every document recreation.(#3844)
         editor.on( 'contentDom', function() {
            editor.editable().on( 'keydown', function( event ) {
               // Do not capture CTRL hotkeys.
               if ( !event.data.$.ctrlKey && !event.data.$.metaKey )
                  undoManager.type( event );
            });
         });

         // Always save an undo snapshot - the previous mode might have
         // changed editor contents.
         editor.on( 'beforeModeUnload', function() {
            editor.mode == 'wysiwyg' && undoManager.save( true );
         });

         function toggleUndoManager() {
				undoManager.enabled = editor.readOnly ? false : editor.mode == 'wysiwyg';
				undoManager.onChange();
			}

			// Make the undo manager available only in wysiwyg mode.
			editor.on( 'mode', toggleUndoManager );

			// Disable undo manager when in read-only mode.
			editor.on( 'readOnly', toggleUndoManager );

         if ( editor.ui.addButton ) {
            editor.ui.addButton( 'Undo', {
               label: editor.lang.undo.undo,
               command: 'undo',
               toolbar: 'undo,10'
            });

            editor.ui.addButton( 'Redo', {
               label: editor.lang.undo.redo,
               command: 'redo',
               toolbar: 'undo,20'
            });
         }

         editor.resetUndo = function() {
            // Reset the undo stack.
            undoManager.reset();

            // Create the first image.
            editor.fire( 'saveSnapshot' );
         };

         editor.on( 'updateSnapshot', function() {
            if ( undoManager.currentImage )
               undoManager.update();
         });

         editor.on( 'lockSnapshot', undoManager.lock, undoManager );

         editor.on( 'unlockSnapshot', undoManager.unlock, undoManager );
      }
   });

   VED.plugins.undo = {};

   // Attributes that browser may changing them when setting via innerHTML.
   var protectedAttrs = /\b(?:href|src|name)="[^"]*?"/gi;

   var Image = VED.plugins.undo.Image = new Class({
      initialize: function( editor ) {
         this.editor = editor;

         editor.fire( 'beforeUndoImage' );

         var contents = editor.getSnapshot(),
            selection = contents && editor.getSelection();

         // In IE, we need to remove the expando attributes.
         VED.env.ie && contents && ( contents = contents.replace( /\s+data-ved-expando=".*?"/g, '' ) );

         this.contents = contents;
         this.bookmarks = selection && selection.createBookmarks2( true );

         editor.fire( 'afterUndoImage' );
      },

      equals: function( otherImage, contentOnly ) {

         var thisContents = this.contents,
            otherContents = otherImage.contents;

         // For IE6/7 : Comparing only the protected attribute values but not the original ones.(#4522)
         if ( VED.env.ie && ( VED.env.ie7Compat || VED.env.ie6Compat ) ) {
            thisContents = thisContents.replace( protectedAttrs, '' );
            otherContents = otherContents.replace( protectedAttrs, '' );
         }

         if ( thisContents != otherContents )
            return false;

         if ( contentOnly )
            return true;

         var bookmarksA = this.bookmarks,
            bookmarksB = otherImage.bookmarks;

         if ( bookmarksA || bookmarksB ) {
            if ( !bookmarksA || !bookmarksB || bookmarksA.length != bookmarksB.length )
               return false;

            for ( var i = 0; i < bookmarksA.length; i++ ) {
               var bookmarkA = bookmarksA[ i ],
                  bookmarkB = bookmarksB[ i ];

               if ( bookmarkA.startOffset != bookmarkB.startOffset || bookmarkA.endOffset != bookmarkB.endOffset || !VED.tools.arrayCompare( bookmarkA.start, bookmarkB.start ) || !VED.tools.arrayCompare( bookmarkA.end, bookmarkB.end ) ) {
                  return false;
               }
            }
         }

         return true;
      }
   });

   var editingKeyCodes = { /*Backspace*/8:1,/*Delete*/46:1 },
      modifierKeyCodes = { /*Shift*/16:1,/*Ctrl*/17:1,/*Alt*/18:1 },
      navigationKeyCodes = { 37:1,38:1,39:1,40:1 }; // Arrows: L, T, R, B

   var UndoManager = new Class({
      initialize: function UndoManager( editor ) {
         this.editor = editor;

         // Reset the undo stack.
         this.reset();
      },

      /**
       * Process undo system regard keystrikes.
       */
      type: function( event ) {
         var keystroke = event && event.data.getKey(),
            isModifierKey = keystroke in modifierKeyCodes,
            isEditingKey = keystroke in editingKeyCodes,
            wasEditingKey = this.lastKeystroke && this.lastKeystroke in editingKeyCodes,
            sameAsLastEditingKey = isEditingKey && this.lastKeystroke && keystroke == this.lastKeystroke,
            // Keystrokes which navigation through contents.
            isReset = keystroke in navigationKeyCodes,
            wasReset = this.lastKeystroke && this.lastKeystroke in navigationKeyCodes,

            // Keystrokes which just introduce new contents.
            isContent = ( !isEditingKey && !isReset ),

            // Create undo snap for every different modifier key.
            modifierSnapshot = ( isEditingKey && !sameAsLastEditingKey ),
            // Create undo snap on the following cases:
            // 1. Just start to type .
            // 2. Typing some content after a modifier.
            // 3. Typing some content after make a visible selection.
            startedTyping = !( isModifierKey || this.typing ) || ( isContent && ( wasEditingKey || wasReset ) );

         if ( startedTyping || modifierSnapshot ) {
            var beforeTypeImage = new Image( this.editor ),
               beforeTypeCount = this.snapshots.length;

            // Use setTimeout, so we give the necessary time to the
            // browser to insert the character into the DOM.
            VED.tools.setTimeout( function() {
               var currentSnapshot = this.editor.getSnapshot();

               // In IE, we need to remove the expando attributes.
               if ( VED.env.ie )
                  currentSnapshot = currentSnapshot.replace( /\s+data-ved-expando=".*?"/g, '' );

               // If changes have taken place, while not been captured yet (#8459),
               // compensate the snapshot.
               if ( beforeTypeImage.contents != currentSnapshot && beforeTypeCount == this.snapshots.length ) {
                  // It's safe to now indicate typing state.
                  this.typing = true;

                  // This's a special save, with specified snapshot
                  // and without auto 'fireChange'.
                  if ( !this.save( false, beforeTypeImage, false ) )
                     // Drop future snapshots.
                     this.snapshots.splice( this.index + 1, this.snapshots.length - this.index - 1 );

                  this.hasUndo = true;
                  this.hasRedo = false;

                  this.typesCount = 1;
                  this.modifiersCount = 1;

                  this.onChange();
               }
            }, 0, this );
         }

         this.lastKeystroke = keystroke;

         // Create undo snap after typed too much (over 25 times).
         if ( isEditingKey ) {
            this.typesCount = 0;
            this.modifiersCount++;

            if ( this.modifiersCount > 25 ) {
               this.save( false, null, false );
               this.modifiersCount = 1;
            }
         } else if ( !isReset ) {
            this.modifiersCount = 0;
            this.typesCount++;

            if ( this.typesCount > 25 ) {
               this.save( false, null, false );
               this.typesCount = 1;
            }
         }

      },

      /**
       * Reset the undo stack.
       */
      reset: function() {
         // Remember last pressed key.
         this.lastKeystroke = 0;

         // Stack for all the undo and redo snapshots, they're always created/removed
         // in consistency.
         this.snapshots = [];

         // Current snapshot history index.
         this.index = -1;

         this.limit = this.editor.config.undoStackSize || 20;

         this.currentImage = null;

         this.hasUndo = false;
         this.hasRedo = false;
         this.locked = null;

         this.resetType();
      },

      /**
       * Reset all states about typing.
       */
      resetType: function() {
         this.typing = false;
         delete this.lastKeystroke;
         this.typesCount = 0;
         this.modifiersCount = 0;
      },

      fireChange: function() {
         this.hasUndo = !!this.getNextImage( true );
         this.hasRedo = !!this.getNextImage( false );
         // Reset typing
         this.resetType();
         this.onChange();
      },

      /**
       * Save a snapshot of document image for later retrieve.
       */
      save: function( onContentOnly, image, autoFireChange ) {
         // Do not change snapshots stack when locked.
         if ( this.locked )
            return false;

         var snapshots = this.snapshots;

         // Get a content image.
         if ( !image )
            image = new Image( this.editor );

         // Do nothing if it was not possible to retrieve an image.
         if ( image.contents === false )
            return false;

         // Check if this is a duplicate. In such case, do nothing.
         if ( this.currentImage && image.equals( this.currentImage, onContentOnly ) )
            return false;

         // Drop future snapshots.
         snapshots.splice( this.index + 1, snapshots.length - this.index - 1 );

         // If we have reached the limit, remove the oldest one.
         if ( snapshots.length == this.limit )
            snapshots.shift();

         // Add the new image, updating the current index.
         this.index = snapshots.push( image ) - 1;

         this.currentImage = image;

         if ( autoFireChange !== false )
            this.fireChange();
         return true;
      },

      restoreImage: function( image ) {
         // Bring editor focused to restore selection.
         var editor = this.editor,
            sel;

         if ( image.bookmarks ) {
            editor.focus();
            // Retrieve the selection beforehand. (#8324)
            sel = editor.getSelection();
         }

         // Start transaction - do not allow any mutations to the
         // snapshots stack done when selecting bookmarks (much probably
         // by selectionChange listener).
         this.locked = 1;

         this.editor.loadSnapshot( image.contents );

         if ( image.bookmarks )
            sel.selectBookmarks( image.bookmarks );
         else if ( VED.env.ie ) {
            // IE BUG: If I don't set the selection to *somewhere* after setting
            // document contents, then IE would create an empty paragraph at the bottom
            // the next time the document is modified.
            var $range = this.editor.document.getBody().$.createTextRange();
            $range.collapse( true );
            $range.select();
         }

         this.locked = 0;

         this.index = image.index;

         // Update current image with the actual editor
         // content, since actualy content may differ from
         // the original snapshot due to dom change. (#4622)
         this.update();
         this.fireChange();
      },

      // Get the closest available image.
      getNextImage: function( isUndo ) {
         var snapshots = this.snapshots,
            currentImage = this.currentImage,
            image, i;

         if ( currentImage ) {
            if ( isUndo ) {
               for ( i = this.index - 1; i >= 0; i-- ) {
                  image = snapshots[ i ];
                  if ( !currentImage.equals( image, true ) ) {
                     image.index = i;
                     return image;
                  }
               }
            } else {
               for ( i = this.index + 1; i < snapshots.length; i++ ) {
                  image = snapshots[ i ];
                  if ( !currentImage.equals( image, true ) ) {
                     image.index = i;
                     return image;
                  }
               }
            }
         }

         return null;
      },

      /**
       * Check the current redo state.
       *
       * @returns {Boolean} Whether the document has previous state to retrieve.
       */
      redoable: function() {
         return this.enabled && this.hasRedo;
      },

      /**
       * Check the current undo state.
       *
       * @returns {Boolean} Whether the document has future state to restore.
       */
      undoable: function() {
         return this.enabled && this.hasUndo;
      },

      /**
       * Perform undo on current index.
       */
      undo: function() {
         if ( this.undoable() ) {
            this.save( true );

            var image = this.getNextImage( true );
            if ( image )
               return this.restoreImage( image ), true;
         }

         return false;
      },

      /**
       * Perform redo on current index.
       */
      redo: function() {
         if ( this.redoable() ) {
            // Try to save. If no changes have been made, the redo stack
            // will not change, so it will still be redoable.
            this.save( true );

            // If instead we had changes, we can't redo anymore.
            if ( this.redoable() ) {
               var image = this.getNextImage( false );
               if ( image )
                  return this.restoreImage( image ), true;
            }
         }

         return false;
      },

      /**
       * Update the last snapshot of the undo stack with the current editor content.
       */
      update: function() {
         // Do not change snapshots stack is locked.
         if ( !this.locked )
            this.snapshots.splice( this.index, 1, ( this.currentImage = new Image( this.editor ) ) );
      },

      /**
		 * Lock the snapshot stack to prevent any save/update operations, and additionally
		 * update the tip snapshot with the DOM changes during the locked period when necessary,
		 * after the {@link #unlock} method is called.
		 *
		 * It's mainly used for ensure any DOM operations that shouldn't be recorded (e.g. auto paragraphing).
		 *
		 * **Note:** For every `lock` call you must call {@link #unlock} once to unlock the Undo Manager.
		 *
		 * @since 4.0
		 */
      lock: function() {
			if ( !this.locked ) {
				var imageBefore = new Image( this.editor );

				// If current editor content matches the tip of snapshot stack,
				// the stack tip must be updated by unlock, to include any changes made
				// during this period.
				var matchedTip = this.currentImage && this.currentImage.equals( imageBefore, true );

				this.locked = { update: matchedTip ? imageBefore : null, level: 1 };
			}
			// Increase the level of lock.
			else
				this.locked.level++;
		},

      /**
		 * Unlock the snapshot stack and check to amend the last snapshot.
		 *
		 * See {@link #lock} for more details.
		 *
		 * @since 4.0
		 */
		unlock: function() {
			if ( this.locked ) {
				// Decrease level of lock and check if equals 0, what means that undoM is completely unlocked.
				if ( !--this.locked.level ) {
					var updateImage = this.locked.update;

					this.locked = null;

					if ( updateImage && !updateImage.equals( new Image( this.editor ), true ) )
						this.update();
				}
			}
		}
   });
})();






